-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove event/emit and introduce ctx.emit that works with the Emittable trait #772
Conversation
f91cf57
to
c258991
Compare
f1d07b6
to
a29de31
Compare
a8d046a
to
53be4fe
Compare
I have no idea where this "CircleCI Pipeline" job comes from. We don't use CircleCI 🤷 |
d021e09
to
f934794
Compare
This is now rebased on top of latest master |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read through the code and think we should discuss the whole design before getting into the details.
Stopgap solutions should not affect a large part of a codebase, but this implementation seems to do so.
I think it would be enough to add a single small module that provides
- semantic checks for the
event
features like the number ofindexed
fields - automatic implementation of the
Emittable
trait to types
What's in my mind is this module should essentially be a temporary alternative to the actual event
procedural macro implementation until we provide a proc-macro-like feature, and the module will simply be removed when the day comes instead of re-modifying the various parts of the analyzer.
Thanks for the review @Y-Nak! I'm happy to discuss any alternative designs!
I think most of the parts that this PR touches are because of the places that remove the current
I'm not arguing against implementing such a module but these two pieces seem to be very minor tweaks to the existing code so I'm not sure how problematic it realistically could be 🙃 e.g. fe/crates/analyzer/src/namespace/items.rs Lines 1882 to 1884 in f934794
Anyway, as I said, happy to discuss what you have in mind. I just wanted to highlight that I think most code changes in the PR are because of the removal of the current |
4007ff9
to
d8fb1e9
Compare
d8fb1e9
to
a5ca513
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I enumerated the #event/#indexed
specific code in the change. It seems still scattered for me. Also, almost all changes happen in the public functions, which means they potentially affect the broader part of the codebase.
pub const EMITTABLE_TRAIT_NAME: &str = "Emittable"; | ||
pub const EMIT_FN_NAME: &str = "emit"; | ||
pub const INDEXED: &str = "indexed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are #event/#indexed
specific and can be used from any code in the fe compiler because it's public
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could move these to a place closer to their usage. My reasoning for putting them here is that they are simple strings and I dislike using magic strings inline because it is easy to mistype a string without the compiler noticing (e.g. "inexed" but you can not mistype INDEXED
without the compiler noticing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should INDEXED
or EMITTABLE
be used from several modules? I think exposing constants in an arbitrary way is not good especially if you know these constants are used as a stopgap. How long do you think these constants will remain?
if field.is_indexed(db) { | ||
indexed_count += 1; | ||
} | ||
|
||
// Multiple attributes are currently still rejected by the parser so we only need to check the name here | ||
if !field.attributes(db).is_empty() && !field.is_indexed(db) { | ||
let span = field.data(db).ast.kind.attributes.first().unwrap().span; | ||
scope.error( | ||
"Invalid attribute", | ||
span, | ||
"illegal name. Only `indexed` supported.", | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto.
Also, the #event/#indexed
specific check would be bloated for additional semantic checks, e.g., we have to check whether a field is encodable because some types are not encodable like enum
.
if indexed_count > MAX_INDEXED_EVENT_FIELDS { | ||
let excess_count = indexed_count - MAX_INDEXED_EVENT_FIELDS; | ||
|
||
let mut labels = fields | ||
.iter() | ||
.filter_map(|(_, field)| { | ||
field | ||
.is_indexed(db) | ||
.then(|| Label::primary(field.span(db), String::new())) | ||
}) | ||
.collect::<Vec<Label>>(); | ||
labels.last_mut().unwrap().message = format!("{} indexed fields", indexed_count); | ||
|
||
scope.fancy_error( | ||
&format!( | ||
"more than three indexed fields in `event {}`", | ||
struct_.name(db) | ||
), | ||
labels, | ||
vec![format!( | ||
"Note: Remove the `indexed` attribute from at least {} {}.", | ||
excess_count, | ||
pluralize_conditionally("field", excess_count) | ||
)], | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto.
pub fn is_indexed(&self, db: &dyn AnalyzerDb) -> bool { | ||
self.attributes(db).contains(&INDEXED.into()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This query is #event/#indexed
specific.
if self.is_std_trait(db, EMITTABLE_TRAIT_NAME) && ty.is_struct(db) { | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks the invariant of a group of methods related to trait implementation because this function returns true
even if the actual implementation doesn't exist. So all other codes which depend on the method would need additional checks to ensure the actual implementation exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even if the actual implementation doesn't exist
Can you elaborate on that? The method is implemented by the compiler (compiler magic basically) for any struct (well, maybe we need to restrict it a bit more because as you hinted structs with enums should maybe not implement Emittable
). For any Emittable
the compiler ensures that emit
can be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the method is properly implemented by the compiler, then why are these awkward lines necessary?
Do these types have ImplId
for the Emittable
trait? I'd like to expect that ImplId
should exist when the is_implemented_for
returns true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no ImplId
because there is no impl
block for it. We can not yet implement the actual event logic in Fe code (in that case, there would be no need for compiler magic at all). The PR doesn't change how events are handled internally, it simply changes how they are triggered.
AnalyzerCallType::TraitValueMethod { | ||
trait_id, method, .. | ||
} if trait_id.is_std_trait(self.db.upcast(), EMITTABLE_TRAIT_NAME) | ||
&& method.name(self.db.upcast()) == EMIT_FN_NAME => | ||
{ | ||
let event = self.lower_method_receiver(func); | ||
self.builder.emit(event, source) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is #event/#indexed
specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, but so is the status quo with the Event
type and the lower_event_type
function. After all, this PR doesn't aim to change the internal handling of events it only changes the trigger from being emit SomeEventType
to being trait based via Emittable.emit(..)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this PR doesn't aim to change the internal handling of events
That's the point. Again, I'd like to ask how long this stopgap solution will remain. And I think the core of the codebase is affected too much just to remove the superficial syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'd like to ask how long this stopgap solution will remain
I don't know but I would assume it is still a while out until we can implement the actual event logic in Fe code natively.
And I think the core of the codebase is affected too much
But wouldn't you agree that the PR actually lowered the complexity of the code base in general? I mean, look at all the event handling logic that was deleted. More code was deleted than added in this PR. It's actually very little code that was added to enable the struct + trait based event handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think complexity could/should be evaluated by the lines of code added/deleted. And how extent the codebase is affected has almost nothing to do with the LoC added/deleted.
I think the way of abstraction introduced here makes the codebase more complicated.
There is no ImplId because there is no impl block for it.
This inconsistency didn't exist before the PR, for example. As I mentioned, this would break the invariant which the trait-related methods should provide, and if the PR is merged as is, it will force us to care about the Emittable
trait wherever we handle traits. You exposed the name of the Emittable
trait as a public constant, and it proves that we should treat the trait specially even now, and in the future, we would start writing other stopgaps to adapt to the unexpected behavior of the core functions. So stopgaps will be chained and expanded.
Also, this PR "hides" the stopgap behind the pseudo abstraction. This makes it difficult to remove these stopgaps when we finally reach the "right" solution. I mean Rust compiler won't complain about the stopgaps in the form of compile errors. So, as a result, we'll have to find and remove them manually. I am not confident in remembering the exact functions and locations where stopgaps are embedded. And as I said, more and more stopgaps will be chained/expanded as time passes.
So, to be honest, I have to say this PR increases the complexity of the code, and that's why I strongly disagree with merging this PR as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, we can try to work out a solution when we co-work in Berlin next week. And if we can not find a satisfying solution than I can live with the current system based on event
and emit
until we have all the ingredients to fully implement something like the Emittable
trait in Fe itself.
That said, I do disagree on the question of complexity. I think having the event
type and the emit
keyword with all of its extra checks is more complex than having a trait in the std lib that is automagically implemented by the compiler as a temporary solution. Especially since it is also ensured that no one else can try to implement it manually for the time being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't disagree with adding the Emittable
trait and implementing it to some types, though I don't think it should be implemented to all types. What I disagree with is the way of realizing it. Again, I still think we shouldn't arbitrarily tweak the core functions on which many other parts depend.
But yeah, we will be able to discuss this face-to-face the next week, so it'd be fine to stop the discussion on GH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added comments, which are not directly related to the current discussion but feel worth noting.
@@ -11,7 +11,7 @@ pub fn mir_lower_struct_all_functions( | |||
struct_ | |||
.all_functions(db.upcast()) | |||
.iter() | |||
.map(|func| db.mir_lowered_func_signature(*func)) | |||
.map(|func| db.mir_lowered_pseudo_monomorphized_func_signature(*func)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems weird to use the function because it is annotated as NOTE: THIS SHOULD ONLY BE USED IN TEST CODE
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes...I guess I forgot that this is also used by the graphviz stuff. I guess what I meant to say is that it should not be used in the actual compilation pipeline. The reason it is needed at all is because the std lib test start failing as soon as the std lib included generic code because the std lib tests iterate over these functions without them being actually called which means we can not monomorphize the generic parameters. So in order to be able to iterate over the functions I created a function that would populate them in a way that maps each generic param to ()
. There's probably a better way that I'm not seeing :)
pub trait Emittable { | ||
fn emit(self, _ val: OutOfReachMarker); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub trait Emittable { | |
fn emit(self, _ val: OutOfReachMarker); | |
} | |
pub trait Emittable { | |
fn emit(self, _ ctx: Context); | |
} |
I think exposing private types from a public function is not a good idea especially when it's a trait function, how about receiving Context
instead of the OutOfReachMarker
type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about receiving
Context
instead of theOutOfReachMarker
type
I thought about that as well but that would mean that people can bypass the ctx.emit(..)
API and instead call MyEvent().emit(ctx)
. That may be fine (since they are still depending on the Context
but I kinda like it more if we force them to go through ctx.emit(..)
. But I don't have very strong feelings on this.
What was wrong?
As described in #717 we want to remove the
event
type and theemit
keyword and make structs usable as events viactx.emit<T: Emittable>(val: T)
How was it fixed?
event
type andemit
statement#indexed
attribute on struct fieldsEmittable
trait which is automatically implemented for all structsemit
trait function internally (just like before)